/*
 * Licensed to Jasig under one or more contributor license agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership. Jasig licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy
 * of the License at the following location:
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package com.baijia;

import com.baijia.authentication.Authentication;
import com.baijia.authentication.AuthenticationManager;
import com.baijia.authentication.MutableAuthentication;
import com.baijia.authentication.handler.exception.AuthenticationException;
import com.baijia.authentication.principal.Credentials;
import com.baijia.authentication.principal.Principal;
import com.baijia.authentication.principal.Service;
import com.baijia.authentication.principal.SimplePrincipal;
import com.baijia.services.RegisteredService;
import com.baijia.services.ServicesManager;
import com.baijia.services.UnauthorizedServiceException;
import com.baijia.ticket.InvalidTicketException;
import com.baijia.ticket.ServiceTicket;
import com.baijia.ticket.TicketCreationException;
import com.baijia.ticket.TicketException;
import com.baijia.ticket.TicketGrantingTicket;
import com.baijia.ticket.TicketGrantingTicketImpl;
import com.baijia.ticket.TicketValidationException;
import com.baijia.ticket.registry.TicketRegistry;
import com.baijia.util.RedisUtil;
import com.baijia.util.UniqueTicketIdGenerator;
import com.baijia.validation.Assertion;
import com.baijia.validation.ImmutableAssertionImpl;

import com.beust.jcommander.internal.Lists;
import com.beust.jcommander.internal.Maps;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @title CentralAuthenticationServiceImpl
 * @desc Passport单点登录核心实现逻辑
 * @author caoliang
 * @date 2015年12月4日
 * @version 1.0
 */
@Component
public final class CentralAuthenticationServiceImpl implements CentralAuthenticationService {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /** 存储Ticket */
    @Autowired
    private TicketRegistry ticketRegistry;

    /** 认证传入的credential */
    @Autowired
    private AuthenticationManager authenticationManager;

    /** 生成唯一的TGT字符串 */
    @Autowired
    private UniqueTicketIdGenerator ticketGrantingTicketUniqueTicketIdGenerator;

    /** 管理注册的业务线service */
    @Autowired
    private ServicesManager servicesManager;

    /**
     * 销毁TGT，并且向各已登录的各业务系统发送logout请求
     *
     * TGT为空，抛出IllegalArgumentException
     */
    @Override
    public void destroyTicketGrantingTicket(final String ticketGrantingTicketId) {
        Assert.notNull(ticketGrantingTicketId);

        logger.debug("Removing ticket [{}] from registry.", ticketGrantingTicketId);
        // 从Redis中取票据，能取到就不过期
        final TicketGrantingTicket ticket =
            (TicketGrantingTicket) this.ticketRegistry.getTicket(ticketGrantingTicketId);

        if (ticket == null) {
            return;
        }

        logger.debug("Ticket found.  Expiring and then deleting.");
        // 票据的过期逻辑，访问各个业务线，使当前用户退出
        ticket.expire();

        // 从Redis中删除票据
        this.ticketRegistry.deleteTicket(ticketGrantingTicketId);
    }

    /**
     * cookie中有TGT，发放ST
     * 
     * @throws AuthenticationException
     * 
     *             TGT或者credential为空抛出IllegalArgumentException
     */
    @Override
    public String grantServiceTicket(final String ticketGrantingTicketId, final Service service,
        final Credentials credentials) throws AuthenticationException, TicketException {

        Assert.notNull(ticketGrantingTicketId, "ticketGrantingticketId cannot be null");
        Assert.notNull(service, "service cannot be null");

        final TicketGrantingTicket ticketGrantingTicket =
            (TicketGrantingTicket) this.ticketRegistry.getTicket(ticketGrantingTicketId);
        // cookie中有票据，但已经在server过期
        if (ticketGrantingTicket == null) {
            throw new InvalidTicketException();
        }

        // 当前允许单点登录产品线
        final RegisteredService registeredService = this.servicesManager.findServiceBy(service);

        if (registeredService == null || !registeredService.isEnabled()) {
            logger.info("ServiceManagement: Unauthorized Service Access. Service [{}] not found in Service Registry.",
                service.getId());
            throw new UnauthorizedServiceException("service没有被授权");
        }

        // 登录信息不为空须校验，TGT中认证信息，必须和新认证的信息一致，否则认证不通过
        if (credentials != null) {
            final Authentication authentication = this.authenticationManager.authenticate(credentials);
            final Authentication originalAuthentication = ticketGrantingTicket.getAuthentication();

            logger.info("Authentication {}, originalAuthentication {}", authentication, originalAuthentication);

            if (!(authentication.getPrincipal().equals(originalAuthentication.getPrincipal()))) {
                destroyTicketGrantingTicket(ticketGrantingTicketId);
                throw new TicketCreationException();
            }
        }

        final ServiceTicket serviceTicket =
            ticketGrantingTicket.grantServiceTicket(
                ticketGrantingTicketUniqueTicketIdGenerator.getNewTicketId(ServiceTicket.PREFIX), service,
                credentials != null);

        this.ticketRegistry.addTicket(serviceTicket);
        RedisUtil.hSet(TicketGrantingTicket.TGTID, serviceTicket.getId(), ticketGrantingTicketId,
            serviceTicket.getExpireTime());

        // 更新存储TGT，TGT内部新增一条ST，登出TGT时使用
        this.ticketRegistry.updateTicket(ticketGrantingTicket);

        // 记录log
        final Authentication authentication = serviceTicket.getGrantingTicket().getAuthentication();
        final String formatString = "Granted %s ticket [%s] for service [%s] for user [%s]";
        final String type;
        final String principalId = authentication.getPrincipal().getId();
        type = "service";
        logger.info(String.format(formatString, type, serviceTicket.getId(), service.getId(), principalId));

        return serviceTicket.getId();
    }

    @Override
    public String grantServiceTicket(final String ticketGrantingTicketId, final Service service)
        throws TicketException, AuthenticationException {
        return this.grantServiceTicket(ticketGrantingTicketId, service, null);
    }

    /**
     * 根据认证凭证，创建TGT
     * 
     * credentials为空抛出IllegalArgumentException
     */
    @Override
    public String createTicketGrantingTicket(final Credentials credentials) throws AuthenticationException {
        Assert.notNull(credentials, "credentials cannot be null");

        final Authentication authentication = authenticationManager.authenticate(credentials);

        final TicketGrantingTicket ticketGrantingTicket =
            new TicketGrantingTicketImpl(
                this.ticketGrantingTicketUniqueTicketIdGenerator.getNewTicketId(TicketGrantingTicket.PREFIX),
                authentication);

        if (credentials.isRememberMe()) {
            this.ticketRegistry.addTicketWithRememberMe(ticketGrantingTicket);
        } else {
            this.ticketRegistry.addTicket(ticketGrantingTicket);
        }

        // 缓存tgt 过期时间
        RedisUtil.hSet(TicketGrantingTicket.TGT_EXPIRED_AT, ticketGrantingTicket.getId(),
            String.valueOf(new Date().getTime() + ticketGrantingTicket.getExpireTime()),
            ticketGrantingTicket.getExpireTime());

        RedisUtil.hSet(Credentials.USER_ROLE, ticketGrantingTicket.getId(), String.valueOf(credentials.getUserRole()),
            49 * 24 * 3600);

        return ticketGrantingTicket.getId();
    }

    /**
     * 验证ST是否合法
     * 
     * ST或者service为空时抛出IllegalArgumentException
     */
    @Override
    public Assertion validateServiceTicket(final String serviceTicketId, final Service service) throws TicketException {
        Assert.notNull(serviceTicketId, "serviceTicketId cannot be null");
        Assert.notNull(service, "service cannot be null");

        logger.info("validate service ticket, serviceTicketId:{}", serviceTicketId);
        final ServiceTicket serviceTicket = (ServiceTicket) this.ticketRegistry.getTicket(serviceTicketId);
        this.ticketRegistry.deleteTicket(serviceTicketId);

        final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
        if (registeredService == null || !registeredService.isEnabled()) {
            logger
                .info(
                    "ServiceManagement: Service does not exist is not enabled, and thus not allowed to validate tickets.   Service: [{}]",
                    service.getId());
            throw new UnauthorizedServiceException("Service not allowed to validate tickets.");
        }

        if (serviceTicket == null) {
            logger.info("ServiceTicket [{}] does not exist.", serviceTicketId);
            throw new InvalidTicketException();
        }

        if (!serviceTicket.isValidFor(service)) {
            logger.info("ServiceTicket [{}] with service [{}] does not match supplied service [{}]", serviceTicketId,
                serviceTicket.getService().getId(), service);
            throw new TicketValidationException(serviceTicket.getService());
        }

        final Authentication authentication = serviceTicket.getGrantingTicket().getAuthentication();
        final Principal principal = authentication.getPrincipal();

        final String principalId = principal.getId();
        final Authentication authToUse;

        if (!registeredService.isIgnoreAttributes()) {
            final Map<String, Object> attributes = Maps.newHashMap();

            for (final String attribute : registeredService.getAllowedAttributes()) {
                final Object value = principal.getAttributes().get(attribute);
                if (value != null) {
                    attributes.put(attribute, value);
                }
            }

            final Principal modifiedPrincipal = new SimplePrincipal(principalId, attributes);
            final MutableAuthentication mutableAuthentication =
                new MutableAuthentication(modifiedPrincipal, authentication.getAuthenticatedDate());
            mutableAuthentication.getAttributes().putAll(authentication.getAttributes());
            mutableAuthentication.getAuthenticatedDate().setTime(authentication.getAuthenticatedDate().getTime());
            authToUse = mutableAuthentication;
        } else {
            final Principal modifiedPrincipal = new SimplePrincipal(principalId, principal.getAttributes());
            authToUse = new MutableAuthentication(modifiedPrincipal, authentication.getAuthenticatedDate());
        }

        final List<Authentication> authentications = Lists.newArrayList();
        authentications.add(authToUse);

        return new ImmutableAssertionImpl(authentications, serviceTicket.getService(), serviceTicket.isFromNewLogin());
    }

    public void setTicketRegistry(final TicketRegistry ticketRegistry) {
        this.ticketRegistry = ticketRegistry;
    }

    public void setAuthenticationManager(final AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    public void setTicketGrantingTicketUniqueTicketIdGenerator(final UniqueTicketIdGenerator uniqueTicketIdGenerator) {
        this.ticketGrantingTicketUniqueTicketIdGenerator = uniqueTicketIdGenerator;
    }

    public void setServicesManager(final ServicesManager servicesManager) {
        this.servicesManager = servicesManager;
    }
}
